/*
 * JpegWriter.java
 *
 */
package pho.prowon.jpeg;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import pho.prowon.ImageDataContainer;

import com.drew.imaging.jpeg.JpegProcessingException;
import com.drew.imaging.jpeg.JpegSegmentReader;

/**
 * Writes a Jpeg file to disk 
 * This class writes a jpeg file as-is with the exception of the IPTC data
 * The contents to be written are either retrieved from the file associated with
 * the passed ImageData object (read from file system) or from the byte array, respectively 
 * InputStream that is passed to one of the constructors.
 * The contents of the file will not be modified with the exception of the IPTC data in the APPD
 * segment. The 8BIM 04 04 segment there within is completely replaced by the data represented by
 * the ImageData object. If no APPD object exists in the current file one is added as last segment
 * before the Start-of-Scan segment.
 * 
 * @author  Peter
 */

public class JpegWriter
{
	/**
	 * Jpeg data from file.
	 */
	private File _file;

	/**
	 * Jpeg data as byte array.
	 */
	private byte[] _data;

	/**
	 * Jpeg data as an InputStream 
	 * this one is finally used as all other variants will be transformed to an appropriate 
	 * InputStream in the getJpegInputStream method
	 */
	private InputStream _stream;

	/**
	 * The ImageData object that is used as source for the IPTC information 
	 */

	private ImageDataContainer _imageData;

	/**
	 * This declaration is private in the JpegSegmentReader class so I have to redeclare it here 
	 * Start-of-Scan
	 */
	private static byte SEGMENT_SOS = (byte) 0xDA;

	/**
	 * This declaration is private in the JpegSegmentReader class so I have to redeclare it here
	 * End-Of-Image 
	 */
	private static byte MARKER_EOI = (byte) 0xD9;

	/**
	 * Creates a JpegWriter retrieving the source from the file that is stored in the 
	 * Filepath of the passed ImageData.
	 * @param imd the ImageData object to be source for both the image and the IPTC data
	 */
	public JpegWriter(ImageDataContainer imd)
	{
		_imageData = imd;
		_stream = null;
		_file = new File(imd.getPathName());
		_data = null;
	}

	/**
	 * Creates a JpegWriter retrieving the source from the file described by the
	 * passed pathname --> the ImageData pathname is only used for output if desired 
	 * (depending on which writeJpegFile method is called) 
	 * @param imd the ImageData object to be source for both the image and the IPTC data
	 * @param pathName the path name of the file containing the rest of the Jpeg data
	 */
	public JpegWriter(ImageDataContainer imd, String pathName)
	{
		_imageData = imd;
		_stream = null;
		_file = new File(pathName);
		_data = null;
	}

	/**
	 * Creates a JpegWriter retrieving the source from the passed bytearray 
	 * @param imd the ImageData object to be source for both the image and the IPTC data
	 * @param fileContents the byte array containing the rest of the Jpeg data
	 */
	public JpegWriter(ImageDataContainer imd, byte[] fileContents)
	{
		_imageData = imd;
		_stream = null;
		_file = null;
		_data = fileContents;
	}

	/**
	 * Creates a JpegWriter retrieving the source from the passed inputstream
	 * @param imd the ImageData object to be source for both the image and the IPTC data
	 * @param in the input stream array containing the rest of the Jpeg data
	 */
	public JpegWriter(ImageDataContainer imd, InputStream in)
	{
		_imageData = imd;
		_stream = in;
		_file = null;
		_data = null;
	}

	/**
	 * Writes the file to the file system. It is using the pathname associated with the
	 * ImageData. To avoid a lock we are reading the complete file in memory first!
	 * @throws FileNotFoundException
	 * @throws JpegProcessingException
	 */
	public void writeJpegFile()
		throws FileNotFoundException, JpegProcessingException
	{
		// initialize the Input Stream to avoid file lock
		BufferedInputStream inStream = getJpegInputStream();
		
		writeJpegFile(new FileOutputStream(_imageData.getPathName()), inStream);
	}

	/**
	 * Writes the file to the file system. It is using the first parameter as pathname 
	 * @param filename Used to write the file
	 * @throws FileNotFoundException
	 * @throws JpegProcessingException
	 */
	public void writeJpegFile(String filename)
		throws FileNotFoundException, JpegProcessingException
	{
		FileOutputStream fos;

		// initialize the Input Stream to avoid file lock
		BufferedInputStream inStream = getJpegInputStream();

		fos = new FileOutputStream(filename);
		writeJpegFile(fos, inStream);
	}

	/**
	 * Writes the file to the passed output stream.  Don't use this method
	 * if you want to save the file under the same name, in this case the 
	 * output stream will have to be closed when we have to read the file first
	 * exception: if this object has already read the file it's ok
	 * @param os Used to write to
	 * @throws JpegProcessingException
	 */
	public void writeJpegFile(OutputStream os)
		throws FileNotFoundException, JpegProcessingException
	{
		// make sure we get the input stream before we open the output stream
		// because if they both point to the same file name we have to read
		// the file contents into a byte array before we start outputting
		//
		// here we cannot influence if the file is already open or not because
		// we don't even know if we are writing to a file at all
		// for consistency we get the input stream nevertheless first		
		BufferedInputStream inStream = getJpegInputStream();
		writeJpegFile(os, inStream);
	}

	/**
	 * Writes the file to the output stream passed as parameter.
	 * @param os Outputstream for writing the Jpeg Image
	 * @param is Inputstream containing the original Jpeg data
	 * @throws FileNotFoundException
	 * @throws JpegProcessingException
	 */
	public void writeJpegFile(OutputStream os, BufferedInputStream is)
		throws FileNotFoundException, JpegProcessingException
	{
		// this is a buffer to store the segment bytes that are just copied
		// to the output 
		byte[] segmentBytes;

		// this is used whenever we need to read 2byte length information
		byte[] segmentLengthBytes = new byte[2];

		// and this one is used to store the extracted segment length
		int segmentLength = 0;

		// used to check the 0xFF value
		byte segmentIdentifier;

		// denotes the current active segment
		byte thisSegmentMarker;

		// used to write the APPD segment if required
		IptcWriter iw;

		// keeps track of the offset from the beginning of the file
		// this is used for diagnostic messages only 
		int offset = 0;

		try
		{
			// first two bytes should be jpeg magic number
			if (!isValidJpegHeaderBytes(is))
			{
				throw new JpegProcessingException("not a jpeg file");
			}
			offset += 2;

			// write jpeg header to the output 
			os.write(0xFF);
			os.write(0xD8);

			// and now we write APPD
			// write to the output 
			os.write(0xFF);

			// this image originally haven't had an APPD segment
			// so we gonna write it now
			// write APPD segment marker to the output 
			os.write(JpegSegmentReader.SEGMENT_APPD & 0xFF);

			// now comes the APPD !!!
			iw = new IptcWriter(_imageData.getImageData());
			iw.writeSegment(os);

			do
			{
				// next byte is 0xFF
				segmentIdentifier = (byte) (is.read() & 0xFF);
				offset++;

				if ((segmentIdentifier & 0xFF) != 0xFF)
				{
					throw new JpegProcessingException(
						"expected jpeg segment start identifier 0xFF at offset "
							+ offset
							+ ", not 0x"
							+ Integer.toHexString(segmentIdentifier & 0xFF));
				}

				// next byte is <segment-marker>
				thisSegmentMarker = (byte) (is.read() & 0xFF);
				offset++;

				if ((thisSegmentMarker & 0xFF) == (SEGMENT_SOS & 0xFF))
				{
					// The 'Start-Of-Scan' segment's length doesn't include the image data, instead would
					// have to search for the two bytes: 0xFF 0xD9 (EOI).
					// It comes last so simply return at this point

					// now we can write the actual image (Start-of-Scan) 
					// write segment marker to the output 
					os.write(0xFF);
					os.write(thisSegmentMarker);

					// write image to the output
					while (is.available() > 0)
					{
						segmentBytes = new byte[is.available()];
						is.read(segmentBytes, 0, is.available());
						os.write(segmentBytes);
					}
					os.close();

					return;
				}
				else if ((thisSegmentMarker & 0xFF) == (MARKER_EOI & 0xFF))
				{
					// the 'End-Of-Image' segment -- this should never be found in this fashion
					throw new JpegProcessingException(
						"unexpected end-of-image identifier 0xD9 at offset "
							+ offset);
				}
				else
				{
					// all the other segments stay untouched								
					// next 2-bytes are <segment-size>: [high-byte] [low-byte]
					is.read(segmentLengthBytes, 0, 2);
					segmentLength =
						((segmentLengthBytes[0] << 8) & 0xFF00)
							| (segmentLengthBytes[1] & 0xFF);

					// segment length includes size bytes, so subtract two
					segmentLength -= 2;

					if (segmentLength > is.available())
					{
						throw new JpegProcessingException("segment size would extend beyond file stream length");
					}

					segmentBytes = new byte[segmentLength];
					is.read(segmentBytes, 0, segmentLength);
					offset += segmentLength;

					// we completely skip the APPD segment and write it at the end
					if (!((thisSegmentMarker & 0xFF)
						== (JpegSegmentReader.SEGMENT_APPD & 0xFF)))
					{
						// write header to the output 
						// this will be executed for all 'regular' segments
						// write to the output 
						os.write(0xFF);
						// write segment marker to the output 
						os.write(thisSegmentMarker);
						os.write(segmentLengthBytes);

						// write segment bytes to the output 
						os.write(segmentBytes);
					}
				}
			}
			while (true);
		}
		catch (IOException ioe)
		{
			throw new JpegProcessingException(
				"IOException processing Jpeg file: " + ioe.getMessage(),
				ioe);
		}
		finally
		{
			try
			{
				if (is != null)
				{
					is.close();
				}
			}
			catch (IOException ioe)
			{
				throw new JpegProcessingException(
					"IOException processing Jpeg file: " + ioe.getMessage(),
					ioe);
			}
		}
	}

	/**
	 * Private helper method to create a BufferedInputStream of Jpeg data from whichever
	 * data source was specified upon construction of this instance.
	 * @return a BufferedInputStream of Jpeg data
	 * @throws JpegProcessingException for any problems obtaining the stream
	 */
	private BufferedInputStream getJpegInputStream()
		throws JpegProcessingException
	{
		if (_stream != null)
		{
			if (_stream instanceof BufferedInputStream)
			{
				return (BufferedInputStream) _stream;
			}
			else
			{
				return new BufferedInputStream(_stream);
			}
		}
		InputStream inputStream;
		if (_data == null)
		{
			try
			{
				inputStream = new FileInputStream(_file);
				_data = new byte[(int) _file.length()];
				inputStream.read(_data);
				inputStream.close();
			}
			catch (FileNotFoundException e)
			{
				throw new JpegProcessingException(
					"Jpeg file does not exist",
					e);
			}
			catch (IOException e)
			{
				throw new JpegProcessingException(
					"Jpeg file cannot be read",
					e);
			}
		}
		inputStream = new ByteArrayInputStream(_data);
		return new BufferedInputStream(inputStream);
	}

	/**
	 * Helper method that validates the Jpeg file's magic number.
	 * This is a copy from the JpegSegmentReader class because it's private there
	 * @param fileStream the InputStream to read bytes from, which must be positioned
	 *        at its start (i.e. no bytes read yet)
	 * @return true if the magic number is Jpeg (0xFFD8)
	 * @throws IOException for any problem in reading the file
	 */
	private boolean isValidJpegHeaderBytes(InputStream fileStream)
		throws IOException
	{
		byte[] header = new byte[2];
		fileStream.read(header, 0, 2);
		return ((header[0] & 0xFF) == 0xFF && (header[1] & 0xFF) == 0xD8);
	}
}
